/*
 * TMap Designer - A Thematic Map design software
 * Copyright (C) 2006 World Health Organization
 * http://www.who.int
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package who.tmap.toolkit.cdr.impl;

import java.awt.Color;
import java.awt.Graphics2D;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import org.apache.log4j.Logger;

import who.tmap.bo.cd.CDColorPattern;
import who.tmap.bo.cd.CartographicDisplay;
import who.tmap.bo.cd.ColorPatternClassification;
import who.tmap.bo.cd.ColorPatternRange;
import who.tmap.common.TMapException;
import who.tmap.toolkit.MappingToolKit;
import who.tmap.toolkit.cdr.ColorPatternRepresentation;
import who.tmap.toolkit.shape.MapShape;
import who.tmap.toolkit.shape.PolygonShape;

public class CDRColorPatternImpl extends BaseCDRImpl implements ColorPatternRepresentation {

    private static Logger logger = Logger.getLogger(CDRColorPatternImpl.class); 

    private CDColorPattern cdc =null;
    private double eqCommonDiff =Double.MIN_VALUE;
    private double qNbValuesPerRange =Double.MIN_VALUE; 
    private Map<Double,Integer> quantiles;
    private Map<String, Integer> rangeForValues =null;
	
	public void init( MappingToolKit mtk, CartographicDisplay cd, double mapFactor ) throws TMapException {
		super.init( mtk, cd, mapFactor );
		
		cdc =(CDColorPattern)cd;
		
		switch( cdc.getClassification() ) {
			case ColorPatternClassification.EQUAL_STEP:
				processEqualStep();
				break;
				
			case ColorPatternClassification.QUANTILES:
				processQuantiles();
				break;
				
			case ColorPatternClassification.USER_DEFINED_RANGES:
				processRanges( cdc );
				break;
				
			case ColorPatternClassification.NON_NUMERIC:
				processNonNumeric();
				break;
				
			default:
				throw new TMapException( "Invalid color/pattern classification id [" + cdc.getClassification() + "]" );
		}		
		
	}
	
	/**
	 * Attributing an index to each caption entry in the CartographicDisplay
	 * @throws TMapException
	 */	
	public void processNonNumeric() throws TMapException {
		// structure that gives indexes for each caption
		rangeForValues =new HashMap<String, Integer>();
		
		if ( cdc.getRanges() != null ) {
			int rangeCt =0;
			for( ColorPatternRange range : cdc.getRanges().values() ) {
				rangeForValues.put( range.getCaption(), rangeCt++ );
			}
		}
		
	}
	
	public void processEqualStep() {
		processRanges( cdc );
		
		eqCommonDiff =( diffVal / 
						(double)(cdc.getRanges() != null ? (double)cdc.getRanges().size() : 1) 
					   );
		
		//NumberFormat formatter = NumberFormat.getNumberInstance();
		
		if ( cdc.getRanges() != null ) {
			int rangeCt =0;
			ColorPatternRange minRange =null, maxRange =null;
			for( ColorPatternRange range : cdc.getRanges().values() ) {
				rangeCt++;
				if ( rangeCt == 1 ) {
					minRange =range;
				}
				maxRange =range;
				
				double from =minVal + ( (eqCommonDiff) * ( range.getRangeIdx() - 1 ) );
				if ( from != Double.MAX_VALUE ) {
					range.setFrom( from );
					range.setTo( (from + eqCommonDiff) );
				} else {
					range.setFrom( 0 );
					range.setTo( 0 );
				}
				
				//logger.debug("processEqualStep: range " + range.getRangeIdx() + " from " + formatter.format( range.getFrom() ) + " to " + formatter.format( range.getTo() ) );
			}
		
			if ( minRange !=null ) {
				minRange.setFrom( minVal );
			}
			
			if ( maxRange != null ) {
				maxRange.setTo( maxVal );
			}
			
		}
		
		resetRangesIfNecessary();
	}
	
	public void processQuantiles() {
		
		processRanges( cdc );
		
		Set<ValueComparableMapShape> sortedQuantiles =new TreeSet<ValueComparableMapShape>();
		Map<MapShape, Object> data =cdc.getPolygonAssociatedData();

		for( MapShape polygon : data.keySet() ) {
			Object val =data.get( polygon );
			
			if ( val != null ) {
				sortedQuantiles.add( new ValueComparableMapShape( polygon, val ) );
			}
		}
		
		ValueComparableMapShape[] qu =new ValueComparableMapShape[ sortedQuantiles.size() ];
		sortedQuantiles.toArray( qu );
		
		int nbRanges =cdc.getRanges() != null ? cdc.getRanges().size() : 1;
		int nbSamples = sortedQuantiles.size();
		
		double[] pn =new double[ nbRanges + 1 ];

		// Nb ranges + 1 values
		for( int ct =0; ct <= nbRanges; ct++ ) {
			pn[ct] =( (double)ct / (double)nbRanges ) * (double)nbSamples;
			
			//logger.debug( "ct=[" + ct + "], pn[ct]=" + pn[ct] );
		}
		
		double[] breakValues =new double[ nbRanges + 1];
		
		for( int ct =0; ct < nbRanges; ct++ ) {
			int idx;
			double pnAbs =Math.abs(pn[ct]);
			if ( pn[ct] - pnAbs >= 0.5 ) {
				idx =(int)pnAbs + 1;
			} else {
				idx =(int)pnAbs;
			}
			
			breakValues[ ct ] =getDoubleValue( qu[ idx ].value.toString() );
			logger.debug( "ct=[" + ct + "], breakValue=[" + breakValues[ ct ] + "]" );
		}
		
		
		for( int ct =0; ct < nbRanges; ct++ ) {			
			ColorPatternRange range =cdc.getRanges().get( ct + 1 );
			if ( ct > 0 ) {
				range.setFrom( breakValues[ ct ] );
			} else {
				range.setFrom( minVal );
			}
			
			if ( ct < nbRanges -1 ) {
				range.setTo( breakValues[ ct + 1 ] );
			} else {
				range.setTo( maxVal );
			}
			
			logger.debug( "range=[" + ct + "], from=[" + range.getFrom() + "], to=[" + range.getTo() + "]" );
		}
		
		quantiles =new HashMap<Double,Integer>();		
		
		for( ValueComparableMapShape vcms : sortedQuantiles ) {
			
			double dbl =getDoubleValue( vcms.value.toString() );
			
			int rangeIdx =-1;
			
			if ( dbl == maxVal ) {
				rangeIdx =nbRanges;
			} else {
				for( ColorPatternRange range : cdc.getRanges().values() ) {
					if ( dbl >= range.getFrom() && dbl <  range.getTo() ) {
						rangeIdx =range.getRangeIdx();
						break;
					}
				}
			}
			
			//logger.debug( "name=" +  ((PolygonShape)vcms.mapShape).getName() + ", range=" + rangeIdx );
			
			quantiles.put( dbl, rangeIdx );
		}
		
		resetRangesIfNecessary();
	}
	
	private void resetRangesIfNecessary() {
		if ( cdc.getRanges() != null ) {
			for( ColorPatternRange range : cdc.getRanges().values() ) {
				if ( range.getFrom() == Double.MAX_VALUE ) {
					range.setFrom( 0 );
				}
				
				if ( range.getTo() == Double.MIN_VALUE ) {
					range.setTo( 0 );
				}
			}
			
		}			
	}
	
	/**
	 * For quantiles sorting
	 * @author stl
	 *
	 */
	@SuppressWarnings("unchecked")
	private class ValueComparableMapShape implements Comparable {
		
		public MapShape mapShape;
		public Object value;
		
		public ValueComparableMapShape( MapShape mapShape, Object value ) {
			this.mapShape =mapShape;
			this.value =value;
		}

		@SuppressWarnings("unchecked")
		public int compareTo(Object o) {
			ValueComparableMapShape other =(ValueComparableMapShape)o;
			return ((Comparable)value).compareTo( (Comparable)other.value );
		}
	}
    
	public int retrieveRange( Graphics2D g, MapShape mapShape ) throws TMapException {
		Integer rangeInt =null;
		ColorPatternRange cb =null;
		Color color =null;
		
		switch( cdc.getClassification() ) {
			case ColorPatternClassification.EQUAL_STEP:
				double val =getDoubleVal( mapShape );
				int range;
				if ( val != Double.MIN_VALUE ) {
					range =(int)( (val - minVal) / eqCommonDiff ) +1;
					
					if ( range < 1 ) {
						range =1;
					}
					
					if ( range > cdc.getRanges().size() ) {
						range =cdc.getRanges().size();
					}
				} else {
					range =cdc.getMissingDataRangeIdx();
				}
				
	
				rangeInt =new Integer( range );
							
				break;
				
			case ColorPatternClassification.QUANTILES:
				val =getDoubleVal( mapShape );
				rangeInt =(Integer)quantiles.get( val );
				
				break;
				
			case ColorPatternClassification.USER_DEFINED_RANGES:
				rangeInt =findUserDefinedRanges( mapShape );
				break;
				
			case ColorPatternClassification.NON_NUMERIC:
				// Gathering the caption value due to the use of the NON_NUMERIC Classification
				Object currentVal =getVal( mapShape );
	
				if ( currentVal != null ) {
					String caption =currentVal.toString();
					Integer theRange =rangeForValues.get( caption.toLowerCase() );
					if ( theRange != null ) {
						rangeInt = theRange + 1;
					}
				}
				break;
				
			default:
				throw new TMapException( "Invalid color/pattern classification id [" + cdc.getClassification() + "]" );
		}
		
		if ( rangeInt == null ) { 
			rangeInt =new Integer( cdc.getMissingDataRangeIdx() );
		}		
		
		cb =cdc.getRanges() != null ? ( ColorPatternRange )cdc.getRanges().get( rangeInt )
								  	: null;

		if ( cb != null ) {
			color =cb.getColor();
		} else {
			color =Color.WHITE;
		}		

		if ( cb !=null && mapShape instanceof PolygonShape ) {

			drawShape( g, ((PolygonShape)mapShape).getGeneralPath(), color, cb.getFillPattern() );

			// To make sure the boundaries are visible
			if ( mtk.isMapVisible() ) {
				((PolygonShape)mapShape).draw( g );
			}
		}
		
		return rangeInt;
	}
	
	public void represent( Graphics2D g, MapShape mapShape ) throws TMapException {
		retrieveRange( g, mapShape );
	}
	
	private Integer findUserDefinedRanges( MapShape mapShape ) {
		Integer rangeInt =null;
		
		double val =getDoubleVal( mapShape );
		
		if ( val != Double.MIN_VALUE ) {
			if ( cdc.getRanges() != null ) {
				for( ColorPatternRange range : cdc.getRanges().values() ) {
					if ( val >= range.getFrom() && val < range.getTo() ) {
						rangeInt =Integer.valueOf( range.getRangeIdx() );
						break;
					}
				}
			}
			
			if ( rangeInt == null && val == maxVal ) {
				rangeInt =cdc.getRanges() != null ? cdc.getRanges().size() : (Integer)null;
			}
		}
		
		return rangeInt;
	}

}
